package server.net.io;

import java.io.IOException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import server.GameServer;
import server.net.ClientConnection;

/**
 * 
 * This class is a wrapper for a java Selector object. Because Selector key-sets
 * are not thread-safe, it is necessary to synchronize on something before
 * registering a connection with the Selector.
 * 
 * The purpose of this class is to allow us to monitor multiple non-blocking
 * IO channels from the different clients that are connected to us. When we
 * receive a signal that we need to read or write to a client, we call
 * ServerHandler on the appropriate NimConnection.
 *
 * @author Vincent Tsuei
 * 
 */
public class Dispatcher implements Runnable {

    /* the game server this dispatcher is attached to */
    private GameServer server;
    
    /* the ID number of this dispatcher */
    private final int dispatcherNumber;
    
    /* the wrapped selector */
    private Selector selector;
    
    /* reentrant lock for concurrency issues */
    private ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
    
    /* if this dispatcher is shutting down */
    private boolean shutdown;
    
    public Dispatcher(GameServer serv, int dispatcherNum) {
        server = serv;
        dispatcherNumber = dispatcherNum;
        try {
            selector = Selector.open();
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }
    
    /**
     * Let the dispatcher know that a write is needed.
     */
    public void notifyWriteNeeded(ClientConnection connection) {
        SelectionKey key = connection.getSocketChannel().keyFor(selector);
        if ((key.interestOps() & SelectionKey.OP_WRITE) > 0) {
            return; //a write is already queued. Wait.
        }
        rwl.writeLock().lock();
        try {
            key.interestOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);
            selector.wakeup();
        } finally {
            rwl.writeLock().unlock();
        }
    }

    /**
     * Register a connection with the Dispatcher. This should be called
     * when a new connection is opened by a client.
     */
    public void register(ClientConnection connection) {
        rwl.writeLock().lock();
        try {
            connection.linkDispatcher(this);
            selector.wakeup();
            try {
                connection.getSocketChannel().register(selector, SelectionKey.OP_READ, connection);
            } catch (ClosedChannelException ex) {
                throw new RuntimeException(ex);
            }
        } finally {
            rwl.writeLock().unlock();
        }
    }
    
    /**
     * Deregister a connection with the Dispatcher. This should be called
     * when a client disconnects by itself or when we want to shutdown a 
     * connection from the server.
     */
    public void deregister(ClientConnection con) {
        rwl.writeLock().lock();
        try {
            con.getSocketChannel().keyFor(selector).cancel();
        } finally {
            rwl.writeLock().unlock();
        }
    }

    /**
     * Get the number of keys (channels) that this dispatcher is tracking.
     * TODO: Figure out if this number is accurate. It should be?
     */
    public int getSelectorKeyCount() {
        rwl.readLock().lock();
        try {
            return selector.keys().size();
        } finally {
            rwl.readLock().unlock();
        }
    }

    /**
     * Notify this Dispatcher to shutdown.
     * TODO: Properly implement this shutdown. @see NimGameServer#shutdown
     */
    public void shutdown() {
        shutdown = true;
        selector.wakeup(); //Unblock selector's select()
    }
    
    
    /**
     * Begin this dispatcher's work loop.
     * Steps:
     *  1. Make sure no clients are reading/writing to our selector's key-set.
     *  2. Block until channels are ready for IO operations.
     *  3. Determine which keys (and their associated NimConnections) are ready for IO.
     *  4. Call the ServerHandler to handle reads/writes.
     *  5. Clear the selector's key-set to let it know we have finished IO.
     */
    @Override
    public void run() {
        while (!shutdown) {
            rwl.writeLock().lock(); //TODO: Figure out of this really works. Seems a little suspicious.
            rwl.writeLock().unlock();
            try {
                int readyForWork = selector.select();
                Logger.getLogger(Dispatcher.class.getName()).log(Level.FINE, "Dispatcher {0} has {1} channels ready for work.", new Object[]{dispatcherNumber, readyForWork});
                Set<SelectionKey> readyKeys = selector.selectedKeys();
                Iterator<SelectionKey> itr = readyKeys.iterator();
                while (itr.hasNext()) {
                    SelectionKey key = itr.next();
                    itr.remove();
                    ClientConnection connection = (ClientConnection) key.attachment();
                    if (key.isValid() && key.isReadable()) {
                        server.getHandler().handleReadEvent(connection);
                    }
                    if (key.isValid() && key.isWritable()) {
                        key.interestOps(SelectionKey.OP_READ);
                        server.getHandler().handleWriteEvent(connection);
                    }
                }
            } catch (IOException ex) {
                Logger.getLogger(Dispatcher.class.getName()).log(Level.SEVERE, "IOException in Dispatcher " + dispatcherNumber + ".", ex);
            } catch (Exception ex) {
                Logger.getLogger(Dispatcher.class.getName()).log(Level.SEVERE, "Exception in Dispatcher " + dispatcherNumber + ".", ex);
            }
        }
    }
}
